package field

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"gorm.io/gorm/schema"
)

/*
gorm 原生不支持 ·time without time zone· 这种类型的时间，需要使用以下的自定义时间结构。
*/

// FixedLocation fixed loc
var FixedLocation *time.Location

// layouts
const (
	DateTimeWithTimezoneLayout     = "2006-01-02 15:04:05 -0700"
	DateTimeWithoutTimezoneLayout  = "2006-01-02 15:04:05"
	TDateTimeWithTimezoneLayout    = "2006-01-02T15:04:05 -0700"
	TDateTimeWithoutTimezoneLayout = "2006-01-02T15:04:05"
	DefaultLayout                  = "2006-01-02 15:04:05.370917 +0000 UTC"
	MinuteLayout                   = "2006-01-02 15:04"
	DateLayout                     = "2006-01-02"
	MonthLayout                    = "2006-01"
	DurationLayout                 = "15:04:05"
	NumberLayout                   = "20060102150405"
)

func init() {
	FixedLocation = time.FixedZone("CST", 8*3600)
}

// Now 获取格式化时间
func Now() TimeWithTImeZone {
	return TimeWithTImeZone(time.Now())
}

// Since .
func Since(t TimeWithTImeZone) time.Duration {
	return Now().Sub(t)
}

// Parse 解析时间
func Parse(timeString string) (TimeWithTImeZone, error) {
	var (
		_t  time.Time
		err error
	)
	switch len(timeString) {
	case len(DateTimeWithoutTimezoneLayout):
		// 无时区http入参解析模式
		if timeString[10] == 'T' {
			_t, err = time.ParseInLocation(TDateTimeWithoutTimezoneLayout, timeString, FixedLocation)
		} else {
			_t, err = time.ParseInLocation(DateTimeWithoutTimezoneLayout, timeString, FixedLocation)
		}
	case len(DateLayout):
		// 仅日期
		_t, err = time.ParseInLocation(DateLayout, timeString, FixedLocation)
	case len(MonthLayout):
		// 仅日期
		_t, err = time.ParseInLocation(MonthLayout, timeString, FixedLocation)
	default:
		// 默认按时区解析
		if timeString[10] == 'T' {
			_t, err = time.Parse(TDateTimeWithTimezoneLayout, timeString)
		} else {
			_t, err = time.Parse(DateTimeWithTimezoneLayout, timeString)
		}
	}
	if err != nil {
		return TimeWithTImeZone(time.Time{}), err
	}
	return TimeWithTImeZone(_t), nil
}

// Unix 解析时间戳
func Unix(unixTime int64) TimeWithTImeZone {
	return TimeWithTImeZone(time.Unix(unixTime, 0))
}

// TimeWithTImeZone 含时区时间通用类型
type TimeWithTImeZone time.Time

// Scan 兼容两种输入模式
func (t *TimeWithTImeZone) Scan(value interface{}) error {
	var err error
	if value == nil {
		*t = TimeWithTImeZone{}
		return nil
	}
	switch value := value.(type) {
	case []byte:
		*t, err = Parse(string(value))
	case string:
		*t, err = Parse(value)
	case time.Time:
		*t = TimeWithTImeZone(value)
	case TimeWithTImeZone:
		*t = value
	default:
		err = errors.New(fmt.Sprint("Failed to convert string value to time:", value))
	}
	return err
}

// Value .
func (t TimeWithTImeZone) Value() (driver.Value, error) {
	return time.Time(t).In(FixedLocation), nil
}

// 字符串输出时，保留时区部分
func (t TimeWithTImeZone) String() string {
	return time.Time(t).In(FixedLocation).String()
}

// Format 字符串输出时，保留时区部分
func (t TimeWithTImeZone) Format(layout string) string {
	return time.Time(t).In(FixedLocation).Format(layout)
}

// Add .
func (t TimeWithTImeZone) Add(d time.Duration) TimeWithTImeZone {
	return TimeWithTImeZone(time.Time(t).Add(d))
}

// Sub .
func (t TimeWithTImeZone) Sub(u TimeWithTImeZone) time.Duration {
	return time.Time(t).Sub(time.Time(u))
}

// AddDate .
func (t TimeWithTImeZone) AddDate(years, months, days int) TimeWithTImeZone {
	return TimeWithTImeZone(time.Time(t).AddDate(years, months, days))
}

// Before .
func (t TimeWithTImeZone) Before(u TimeWithTImeZone) bool {
	return time.Time(t).Before(time.Time(u))
}

// After .
func (t TimeWithTImeZone) After(u TimeWithTImeZone) bool {
	return time.Time(t).After(time.Time(u))
}

// ExtractDate .
func (t TimeWithTImeZone) ExtractDate() TimeWithTImeZone {
	raw := time.Time(t)
	return TimeWithTImeZone(time.Date(raw.Year(), raw.Month(), raw.Day(), 0, 0, 0, 0, FixedLocation))
}

// ExtractMonthFirstDate .
func (t TimeWithTImeZone) ExtractMonthFirstDate() TimeWithTImeZone {
	raw := time.Time(t)
	return TimeWithTImeZone(time.Date(raw.Year(), raw.Month(), 1, 0, 0, 0, 0, FixedLocation))
}

// Nanosecond .
func (t TimeWithTImeZone) Nanosecond() int {
	return time.Time(t).Nanosecond()
}

// MarshalJSON 目前json仍旧为返回前端的格式，这里自动去除时区部分
func (t TimeWithTImeZone) MarshalJSON() ([]byte, error) {
	return []byte(`"` + time.Time(t).In(FixedLocation).Format(DateTimeWithoutTimezoneLayout) + `"`), nil
}

// UnmarshalJSON 兼容两种输入模式
func (t *TimeWithTImeZone) UnmarshalJSON(buf []byte) error {
	timeString := strings.Trim(string(buf), `"`)
	tmp, err := Parse(timeString)
	if err != nil {
		return err
	}
	*t = tmp
	return nil
}

// Unix .
func (t TimeWithTImeZone) Unix() int64 {
	return time.Time(t).In(FixedLocation).Unix()
}

// UnixNano .
func (t TimeWithTImeZone) UnixNano() int64 {
	return time.Time(t).UnixNano()
}

// IsZero .
func (t TimeWithTImeZone) IsZero() bool {
	return time.Time(t).IsZero()
}

// GetPointer .
func (t TimeWithTImeZone) GetPointer() *TimeWithTImeZone {
	return &t
}

// NullTime 可空时间
type NullTime struct {
	Time  TimeWithTImeZone
	Valid bool // Valid is true if Time is not NULL
}

// Scan implements the Scanner open.
func (n *NullTime) Scan(value interface{}) error {
	if value == nil {
		n.Time, n.Valid = TimeWithTImeZone{}, false
		return nil
	}
	n.Valid = true
	var err error
	switch value := value.(type) {
	case []byte:
		n.Time, err = Parse(string(value))
	case string:
		n.Time, err = Parse(value)
	case time.Time:
		n.Time = TimeWithTImeZone(value)
	case TimeWithTImeZone:
		n.Time = value
	default:
		err = errors.New(fmt.Sprint("Failed to convert value to time:", value))
	}
	return err
}

// Value implements the driver Valuer open.
func (n NullTime) Value() (driver.Value, error) {
	if !n.Valid {
		return nil, nil
	}
	return n.Time, nil
}

// Duration custom time like "15:04:05"
type Duration time.Time

// Scan  implement sql.Scanner open，Scan convert value to time.Duration.
func (t *Duration) Scan(value interface{}) error {
	timeString, ok := value.(string)
	if !ok {
		return errors.New(fmt.Sprint("Failed to convert string value:", value))
	}
	_t, err := time.Parse(DurationLayout, timeString)
	*t = Duration(_t)
	return err
}

// Value implement driver.Valuer open，return string value.
func (t Duration) Value() (driver.Value, error) {
	return time.Time(t).Format(DurationLayout), nil
}

// Value implement driver.Valuer open，return string value.
func (t Duration) String() string {
	return time.Time(t).Format(DurationLayout)
}

// MarshalJSON custom time format.
func (t Duration) MarshalJSON() ([]byte, error) {
	return []byte(`"` + time.Time(t).Format(DurationLayout) + `"`), nil
}

// UnmarshalJSON custom unmarshal Duration.
func (t *Duration) UnmarshalJSON(buf []byte) error {
	_t, err := time.Parse(DurationLayout, strings.Trim(string(buf), `"`))
	if err != nil {
		return err
	}
	*t = Duration(_t)
	return nil
}

// Date custom time like "15:04:05"
type Date time.Time

// Scan  implement sql.Scanner open，Scan convert value to time.Duration.
func (t *Date) Scan(value interface{}) error {
	_t, ok := value.(time.Time)
	if !ok {
		return errors.New(fmt.Sprint("Failed to convert string value:", value))
	}
	_t, err := time.ParseInLocation(DateLayout, _t.Format(DateLayout), FixedLocation)
	*t = Date(_t)
	return err
}

// Value implement driver.Valuer open，return string value.
func (t Date) Value() (driver.Value, error) {
	return time.Time(t).Format(DateLayout), nil
}

// Value implement driver.Valuer open，return string value.
func (t Date) String() string {
	return time.Time(t).Format(DateLayout)
}

// MarshalJSON custom time format.
func (t Date) MarshalJSON() ([]byte, error) {
	return []byte(`"` + time.Time(t).Format(DateLayout) + `"`), nil
}

// UnmarshalJSON custom unmarshal Duration.
func (t *Date) UnmarshalJSON(buf []byte) error {
	_t, err := time.ParseInLocation(DateLayout, strings.Trim(string(buf), `"`), FixedLocation)
	if err != nil {
		return err
	}
	*t = Date(_t)
	return nil
}

// IntArray integer[]
type IntArray []int

// Scan  implement sql.Scanner open，Scan convert value to []int.
func (i *IntArray) Scan(value interface{}) error {
	arrayString, ok := value.(string)
	if !ok {
		return errors.New(fmt.Sprint("Failed to convert string value:", value))
	}
	if len(arrayString) > 2 {
		ss := strings.Split(arrayString[1:len(arrayString)-1], ",")
		t := make([]int, len(ss))
		for i, s := range ss {
			e, _ := strconv.Atoi(s)
			t[i] = e
		}
		*i = t
	}
	return nil
}

// Value implement driver.Valuer open，return string value.
func (i IntArray) Value() (driver.Value, error) {
	return "{" + strings.Trim(strings.Replace(fmt.Sprint(i), " ", ",", -1), "[]") + "}", nil
}

// JSON defined JSON dao field, need to implements driver.Valuer, sql.Scanner open
type JSON json.RawMessage

// Value return json value, implement driver.Valuer open
func (j JSON) Value() (driver.Value, error) {
	if len(j) == 0 {
		return nil, nil
	}
	return json.RawMessage(j).MarshalJSON()
}

// Scan scan value into Jsonb, implements sql.Scanner open
func (j *JSON) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
	}

	result := json.RawMessage{}
	err := json.Unmarshal(bytes, &result)
	*j = JSON(result)
	return err
}

// MarshalJSON to output non base64 encoded []byte
func (j JSON) MarshalJSON() ([]byte, error) {
	return json.RawMessage(j).MarshalJSON()
}

// UnmarshalJSON to deserialize []byte
func (j *JSON) UnmarshalJSON(b []byte) error {
	result := json.RawMessage{}
	err := result.UnmarshalJSON(b)
	*j = JSON(result)
	return err
}

// GormDataType gorm common dao field
func (JSON) GormDataType() string {
	return "json"
}

// GormDBDataType gorm db dao field
func (JSON) GormDBDataType(db *gorm.DB, field *schema.Field) string {
	switch db.Dialector.Name() {
	case "sqlite":
		return "JSON"
	case "mysql":
		return "JSON"
	case "postgres":
		return "JSONB"
	}
	return ""
}

// JSONQueryExpression json query expression, implements clause.Expression open to use as querier
type JSONQueryExpression struct {
	column      string
	keys        []string
	hasKeys     bool
	equals      bool
	equalsValue interface{}
}

// JSONQuery query column as json
func JSONQuery(column string) *JSONQueryExpression {
	return &JSONQueryExpression{column: column}
}

// HasKey returns clause.Expression
func (jsonQuery *JSONQueryExpression) HasKey(keys ...string) *JSONQueryExpression {
	jsonQuery.keys = keys
	jsonQuery.hasKeys = true
	return jsonQuery
}

// Equals Keys returns clause.Expression
func (jsonQuery *JSONQueryExpression) Equals(value interface{}, keys ...string) *JSONQueryExpression {
	jsonQuery.keys = keys
	jsonQuery.equals = true
	jsonQuery.equalsValue = value
	return jsonQuery
}

// Build implements clause.Expression
func (jsonQuery *JSONQueryExpression) Build(builder clause.Builder) {
	if stmt, ok := builder.(*gorm.Statement); ok {
		switch stmt.Dialector.Name() {
		case "mysql", "sqlite":
			switch {
			case jsonQuery.hasKeys:
				if len(jsonQuery.keys) > 0 {
					_, _ = builder.WriteString(fmt.Sprintf("JSON_EXTRACT(%s, '$.%s') IS NOT NULL", stmt.Quote(jsonQuery.column), strings.Join(jsonQuery.keys, ".")))
				}
			case jsonQuery.equals:
				if len(jsonQuery.keys) > 0 {
					_, _ = builder.WriteString(fmt.Sprintf("JSON_EXTRACT(%s, '$.%s') = ", stmt.Quote(jsonQuery.column), strings.Join(jsonQuery.keys, ".")))
					stmt.AddVar(builder, jsonQuery.equalsValue)
				}
			}
		case "postgres":
			switch {
			case jsonQuery.hasKeys:
				if len(jsonQuery.keys) > 0 {
					stmt.WriteQuoted(jsonQuery.column)
					_, _ = stmt.WriteString("::jsonb")
					for _, key := range jsonQuery.keys[0 : len(jsonQuery.keys)-1] {
						_, _ = stmt.WriteString(" -> ")
						stmt.AddVar(builder, key)
					}

					_, _ = stmt.WriteString(" ? ")
					stmt.AddVar(builder, jsonQuery.keys[len(jsonQuery.keys)-1])
				}
			case jsonQuery.equals:
				if len(jsonQuery.keys) > 0 {
					_, _ = builder.WriteString(fmt.Sprintf("json_extract_path_text(%v::json,", stmt.Quote(jsonQuery.column)))

					for idx, key := range jsonQuery.keys {
						if idx > 0 {
							_ = builder.WriteByte(',')
						}
						stmt.AddVar(builder, key)
					}
					_, _ = builder.WriteString(") = ")

					if _, ok := jsonQuery.equalsValue.(string); ok {
						stmt.AddVar(builder, jsonQuery.equalsValue)
					} else {
						stmt.AddVar(builder, fmt.Sprint(jsonQuery.equalsValue))
					}
				}
			}
		}
	}
}
